/* License Notice:
**
** This program is free software: you can redistribute it and/or modify
**    it under the terms of the GNU General Public License as published by
**    the Free Software Foundation, either version 3 of the License, or
**    (at your option) any later version.
** This program is distributed in the hope that it will be useful,
**   but WITHOUT ANY WARRANTY; without even the implied warranty of
**   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
**   GNU General Public License for more details.
** You should have received a copy of the GNU General Public License
**   along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

/**
 * @file arcade_fighter.cpp
 * @author TooOld2Rock'nRoll
 * @see arcade_fighter.hpp
 */
/*---- Includes ----*/
#include "arcade_fighter.hpp"

#include "config.h"
#include "toolbox/debug.h"


/*---- Global Variables ----*/


/*---- Methods Implementation ----*/
/**
 * @brief ArcadeFighter base Constructor
 *
 * @warning Must be created before any other class from this library or other OpenGl related objects.
 *
 * @param title - the text displayed as the window decoration title.
 * @param display - which display to create the window? (defaults to zero if display doesn't exist)
 * @param window_flags - modifiers to the game window (SDL_WINDOW_OPENGL implicit), valid values are https://wiki.libsdl.org/SDL2/SDL_WindowFlags
 */
ArcadeFighter::ArcadeFighter (const char *title, unsigned display, SDL_WindowFlags window_flags) : ui_display(display)
{
    int i = 0;
    const char backup_title[] = "ArcadeFighter Window";

    _debug (TRACE, "::%s (title='%s', disp=%d)", __FUNCTION__, title, display);

    if (!title)
        title = backup_title;

    _debug (INFO, "****** Starting the ArcadeFighter Engine for the game '%s' ******", title);

    this->p_sdl_instance = &WindowManager::instance ();
    this->p_sdl_instance->init (title, display, window_flags);

    this->p_gl_instance = &OpenGl::instance ();
    this->p_gl_instance->init (this->p_sdl_instance->getNativeMgrRef ());



    //set default FPS
    this->setTargetFPS (GAME_MAX_FPS);
    _debug (INFO, "Game max FPS set to %u (this may be ignored depending on other settings)", GAME_MAX_FPS);
    this->setTargetGameLogicLoopFPS (GAME_LOGIC_FPS);
    _debug (INFO, "Logic Loop max FPS set to %u (this may be ignored depending on other settings)", GAME_LOGIC_FPS);

    //Star the Audio device.
    Audio::instance ();

    //Get the ResourceManager and set some global/common/default stuff
    this->p_res_mgr_instance = &ResourceManager::instance ();
    this->p_lib_status = &this->p_res_mgr_instance->getGlobalStatus ();
    this->p_lib_status->u_window_width = this->p_sdl_instance->getWindowWidth ();
    this->p_lib_status->u_window_height = this->p_sdl_instance->getWindowHeight ();


    //Get EventManager instance and setup events to listen for
    this->p_ev_mgr_instance = &EventManager::instance();
    this->_event_listener_ID = this->p_ev_mgr_instance->addListener ();

    this->p_ev_mgr_instance->listenForQuitEvent (this->_event_listener_ID);
    this->_hard_exit_command = this->p_ev_mgr_instance->listenForEvent (this->_event_listener_ID, key_state::UP, SDL_SCANCODE_ESCAPE);
    _debug (INFO, "You can exit at any time by pressing the Escape key!");

    this->p_ev_mgr_instance->listenForEvent (this->_event_listener_ID, SDL_WindowEventID::SDL_WINDOWEVENT_SIZE_CHANGED);

    //Get a copy to the Audio singleton and make sure it exists before anyone needs it.
    this->p_audio_instance = &Audio::instance ();
    this->p_audio_instance->open ();

    //Init other class members
    for (i = PLAYERS_COUNT; i--; )
        this->p_players[i] = nullptr;

    _debug (INFO, "***** ArcadeFighter startup completed successfully!!! *****");
}//End Constructor

/**
 * @brief Destructor
 */
ArcadeFighter::~ArcadeFighter ()
{
    _debug (TRACE, "::%s", __FUNCTION__);

    this->p_audio_instance->close ();

    this->p_ev_mgr_instance->removeListener (this->_event_listener_ID);

    _debug (INFO, "***** Closing the ArcadeFighter Engine, By By  o/  *****");
}//End Destructor


/**
 * @brief Command the engine to be terminated gracefully.
 * @remark Resources will only be dealocated at the destructor.
 */
void ArcadeFighter::exit ()
{
    _debug (TRACE, "::%s ()", __FUNCTION__);

    _debug (INFO, "Exit signal received, wrapping things up.........");

    this->b_should_quit = true;

    if ( this->_lv_running)
        this->_lv_running->setState (level_state_e::FINISHED);
}//End exit ()


/**
 * @brief Interface to Input Handle Callback.
 *
 * @param delta_t - time elapsed since last game loop iteration.
 */
void ArcadeFighter::processEvents (double delta_t)
{
    static const EventManager::event_s *ev = nullptr;

    _debug (VERBOSE, "::%s ()", __FUNCTION__);

    while ((ev = this->p_ev_mgr_instance->pop (this->_event_listener_ID)))
    {
        switch (ev->type)
        {
            case EventManager::event_type_e::QUIT:
                this->exit ();
                break;
            //end case EventManager::event_type::QUIT:

            case EventManager::event_type_e::WINDOW:
            {
                static window_event_info_s *win_ev = nullptr;

                win_ev = (window_event_info_s *)ev->p_data;

                ///@todo: change to a switch when we have more cases!! https://wiki.libsdl.org/SDL2/SDL_WindowEvent
                if (win_ev->window_ev_id == SDL_WINDOWEVENT_SIZE_CHANGED)
                {
                    this->p_lib_status->u_window_width = win_ev->i_data1;
                    this->p_lib_status->u_window_height = win_ev->i_data2;
                }//end else if (EventManager::isEqual (ev, this->_window_size_changed))

                break;
            }//end case EventManager::event_type::WINDOW:

            case EventManager::event_type_e::KEYBOARD:
                if (this->_hard_exit_command && EventManager::isEqual (*ev, *this->_hard_exit_command))
                    this->exit ();

                break;
            //end case EventManager::event_type::KEYBOARD:

            default:
                //something not expected, ignore it.
                _debug (WARNING, "Unexpected event -> %u", ev->type);
                break;
        }//end switch (ev->_type)
    }//end while (this->evMngInstance.fetchEvent (this->lsID))
}//End processEvents ()


/**
 * @brief Sets a player to be used on levels.
 *
 * @param pl_number - the player number.
 * @param pl - reference to the player.
 *
 * @throws std::out_of_range - if player number is higher than the maximum configured count.
 */
void ArcadeFighter::setPlayer (unsigned pl_number, Player *pl)
{
    _debug (TRACE, "::%s (player=%u)", __FUNCTION__, pl_number);

    if (pl_number >= PLAYERS_COUNT)
        throw std::out_of_range (fmt::format ("{0}:{1} - Max number of player is {2}, but trying to set player nº{3}!", __LINE__, __FILE__, PLAYERS_COUNT, pl_number));

#ifdef ENABLE_DEBUG
    if (!pl)
        _debug (WARNING, "Setting a null character to the level, are you sure?");
    if (this->p_players[pl_number])
        _debug (WARNING, "Player already set, are you sure you want to override it?");
#endif

    this->p_players[pl_number] = pl;
}//End setPlayer ()


/**
 * @brief Load a Level and start the game loop under the library control.
 * @remark The level will enter the LOADING state as it starts and will exit when FINISH state is set.
 *
 * @param lv - the Level to load/start
 * @param style - which flavor of game loop to use?
 *
 * @throws std::runtime_error - not all players have been set.
 * @throws std::runtime_error - if the game loop style is invalid.
 */
void ArcadeFighter::levelStart (Level &lv, ArcadeFighter::delta_time_style_e style)
{
    int i = 0;

    _debug (TRACE, "::%s (style=%u)", __FUNCTION__, style);

    for (i = PLAYERS_COUNT; i--; )
    {
        if (!this->p_players[i])
            _debug (WARNING, "Starting level without a Player%u, are you sure?", i);
    }//end for (i = PLAYERS_COUNT; i--; )

    this->_lv_running = &lv;
    lv.load ();

    switch (style)
    {
        case delta_time_style_e::FIXED:
            this->run_fixedDeltaTime (lv);
            break;
        //end case delta_time_style::FIXED:

        case delta_time_style_e::VARIABLE:
            this->run_variableDeltaTime (lv);
            break;
        //end case delta_time_style::VARIABLE:

        case delta_time_style_e::SEMI_FIXED:
            this->run_semiFixedDeltaTime (lv);
            break;
        //end case delta_time_style::SEMI_FIXED:

        case delta_time_style_e::CHUNKY:
            this->run_chunkyDeltaTime (lv);
            break;
        //end case delta_time_style::CHUNKY:

        case delta_time_style_e::_max_delta_style:
        default:
            throw std::runtime_error (fmt::format ("{0}:{1} - Unknown Style for the Game Loop, this is bad, shouldn't be here!", __LINE__, __FILE__));
            break;
    }//end switch (style)

    this->_lv_running = nullptr;
}//End start ()

/**
 * @brief Subroutine to handle only the event processing of the game loop.
 *
 * @param lv - The level running.
 * @param delta_t - time elapsed since the last loop.
 */
void ArcadeFighter::loop_section_events (Level &lv, double delta_t)
{
    int i = 0;

    this->processEvents (delta_t); //handle high level events for the game itself
    for (i = PLAYERS_COUNT; i--; )
    {
        if (this->p_players[i])
            this->p_players[i]->processEvents (delta_t);
    }//end for (i = PLAYERS_COUNT; i--; )
    lv.processEvents (delta_t); //handle events for the level itself
}//end loop_section_events ()

/**
 * @brief Subroutine to handle only the logic and state updates of the game loop.
 *
 * @param lv - The level running.
 * @param delta_t - time elapsed since the last loop.
 */
void ArcadeFighter::loop_section_logic (Level &lv, double delta_t)
{
    int i = 0;

    lv.update (delta_t); //update the state of the level itself
    for (i = PLAYERS_COUNT; i--; )
    {
        if (this->p_players[i])
            this->p_players[i]->update (delta_t);
    }//end for (i = PLAYERS_COUNT; i--; )
}//end loop_section_logic

/**
 * @brief Subroutine to handle only rendering of the game loop.
 *
 * @param lv - The level running.
 * @param delta_t - time elapsed since the last loop.
 */
void ArcadeFighter::loop_section_render (Level &lv, double delta_t)
{
    int i = 0;

    lv.draw (delta_t); //draw components of the level itself
    for (i = PLAYERS_COUNT; i--; )
    {
        if (this->p_players[i])
            this->p_players[i]->draw (delta_t);
    }//end for (i = PLAYERS_COUNT; i--; )
}//end loop_section_render ()

/**
 * @brief Runs the game loop on a fixed FPS regime.
 *
 * @param lv - the Level to run.
 */
void ArcadeFighter::run_fixedDeltaTime (Level &lv)
{
    double delta_t = 0.f;

    _debug (TRACE, "::%s ()", __FUNCTION__);

    while (lv.getState () != level_state_e::FINISHED)
    {
        delta_t = getDeltaTime (&this->_main_delta_t, true);

        /** Process Events **/
        this->p_ev_mgr_instance->update (); //be sure this is the first method to be called since it fetches all the system events.
        this->loop_section_events (lv, delta_t);

        /** Update Level Objects State **/
        this->loop_section_logic (lv, delta_t);

        /** Draw Level Objects **/
        glClear (GL_COLOR_BUFFER_BIT); //clear screen
        this->loop_section_render (lv, delta_t);
        this->p_sdl_instance->swapWindow ();

        //consume the rest of the time remaining from last reset
        delta_t = getDeltaTime (&this->_main_delta_t, false);
        if (delta_t < this->f_target_delta_t)
            sleep (this->f_target_delta_t - delta_t);
        else //need to free the thread to give time for other processes
            sleep (0.000001);

        //printf ("FPS (fixed): target=%.4f -- real=%.4f\r", 1.f/this->f_target_delta_t, 1.0/getDeltaTime (&this->_main_delta_t, false)); //<-- debug
    }//end while (lv.getState () != level_state_t::FINISHED)
}//End run_fixedDeltaTime ()

/**
 * @brief Runs the game loop as fast as possible.
 * @remark It will most likely be bound by VSYNK.
 *
 * @param lv - the Level to run.
 */
void ArcadeFighter::run_variableDeltaTime (Level &lv)
{
    double delta_t = 0;

    _debug (TRACE, "::%s ()", __FUNCTION__);

    while (lv.getState () != level_state_e::FINISHED)
    {
        delta_t = getDeltaTime (&this->_main_delta_t, true);

        /** Process Input **/
        this->p_ev_mgr_instance->update (); //be sure this is the first method to be called since it fetches all the system events.
        this->loop_section_events (lv, delta_t);

        /** Update Level Objects State**/
        this->loop_section_logic (lv, delta_t);

        /** Draw Level Objects **/
        glClear (GL_COLOR_BUFFER_BIT); //clear screen
        this->loop_section_render (lv, delta_t);
        this->p_sdl_instance->swapWindow ();

        //need to free the thread to give time for other processes
        sleep (0.000001);

        //printf ("FPS (variable): target=∞ -- real=%.4f\r", 1.0/getDeltaTime (&this->_main_delta_t, false)); //<-- debug
    }//end while (lv.getState () != level_state_t::FINISHED)
}//End run_variabbleDeltaTime ()

/**
 * @brief Runs the logic section as fast as possible and the rendering is FPS limited.
 *
 * @param lv - the Level to run.
 */
void ArcadeFighter::run_semiFixedDeltaTime (Level &lv)
{
    double delta_t = 0,
           inner_t = 0;

    _debug (TRACE, "::%s ()", __FUNCTION__);

    while (lv.getState () != level_state_e::FINISHED)
    {
        delta_t = getDeltaTime (&this->_main_delta_t, true);
        inner_t = getDeltaTime (&this->_inner_delta_t, true);

        //let objects update freely, until target is reached.
        do {
            /** Process Input **/
            this->p_ev_mgr_instance->update (); //be sure this is the first method to be called since it fetches all the system events.
            this->loop_section_events (lv, inner_t);

            /** Update Level Objects State**/
            this->loop_section_logic (lv, inner_t);

            //this loop runs unbound and may do heavy processing, need to free the thread to give time for other processes
            sleep (0.000001);

            inner_t = getDeltaTime (&this->_inner_delta_t, true);
        } while (getDeltaTime(&this->_main_delta_t, false) + inner_t < this->f_target_delta_t);

        /** Draw Level Objects **/
        glClear (GL_COLOR_BUFFER_BIT); //clear screen
        this->loop_section_render (lv, delta_t);
        this->p_sdl_instance->swapWindow ();

        //printf ("FPS (semi-fixed): target=%.4f -- real=%.4f (inner=%.4f)\r", 1.f/this->f_target_delta_t, 1.0/this->getDeltaTime(&false), 1.0/inner_t); //<-- debug
    }//end while (lv.getState () != level_state_t::FINISHED)
}//End run_semiFixedDeltaTime

/**
 * @brief Runs the game logic and rendering in two different FPS.
 *
 * @param lv - the Level to run.
 */
void ArcadeFighter::run_chunkyDeltaTime (Level &lv)
{
    double delta_t = 0,
           inner_t = 0;
    double accumulator = 0;

    _debug (TRACE, "::%s ()", __FUNCTION__);

    while (lv.getState () != level_state_e::FINISHED)
    {
        delta_t = getDeltaTime (&this->_main_delta_t, true);
        inner_t = getDeltaTime (&this->_inner_delta_t, true);
        accumulator = this->f_target_delta_t;

        //update objects a set number of times till target is reached
        do {
            /** Process Input **/
            this->p_ev_mgr_instance->update (); //be sure this is the first method to be called since it fetches all the system events.
            this->loop_section_events (lv, inner_t);

            /** Update Level Objects State **/
            this->loop_section_logic (lv, inner_t);

            //consume the rest of the time remaining from last reset
            inner_t = getDeltaTime (&this->_inner_delta_t, false);
            if (inner_t < this->f_target_step_t)
                sleep (this->f_target_step_t - inner_t);

            //need to free the thread to give time for other processes
            sleep (0.000001);

            inner_t = getDeltaTime (&this->_inner_delta_t, true);
            accumulator -= inner_t;
        } while (getDeltaTime(&this->_main_delta_t, false) + inner_t < this->f_target_delta_t);

        //one last tick to consume % of target step for inner loop
        if (accumulator > 0)
        {
            /** Process Input **/
            this->p_ev_mgr_instance->update (); //be sure this is the first method to be called since it fetches all the system events.
            this->loop_section_events (lv, accumulator);

            /** Update Level Objects State**/
            this->loop_section_logic (lv, accumulator);
        }//end if (accumulator > 0)

        /** Draw Level Objects **/
        glClear (GL_COLOR_BUFFER_BIT); //clear screen
        this->loop_section_render (lv, delta_t);
        this->p_sdl_instance->swapWindow ();

        //consume the rest of the time remaining from last reset
        //some proportions of Render FPS to Update Loop FPS need this procedure
        delta_t = getDeltaTime (&this->_main_delta_t, false);
        if (delta_t < this->f_target_delta_t)
            sleep (this->f_target_delta_t - delta_t);
        else //need to free the thread to give time for other processes
            sleep (0.000001);

        //printf ("FPS (chunky): target=%.4f -- real=%.4f (inner: target=%.4f -- real=%.4f -- remain=%.4fsec)\r", 1.f/this->f_target_delta_t, 1.0/this->getDeltaTime(&false), 1.f/this->f_target_step_t, 1.0/inner_t, accumulator); //<-- debug
    }//end while (lv.getState () != level_state_t::FINISHED)
}//End run_ChunkyDeltaTime ()

